package com.crowdgeeks.twitterclient;

import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import twitter4j.Paging;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;

@SuppressWarnings("serial")
public class TwitterWebClientServlet extends HttpServlet {
    private static final Logger logger = Logger.getLogger(TwitterWebClientServlet.class.getName());

    private static final String INIT_PARAM_CONSUMER_KEY = "consumerKey";

    private static final String INIT_PARAM_CONSUMER_SECRET = "consumerSecret";

    private static String consumerKey;

    private static String consumerSecret;

    private static final String PARAM_ACTION = "action";

    private static final String ACTION_SIGN_IN = "sign_in";

    private static final String ACTION_UPDATE = "update";

    private static final String ACTION_DELETE = "delete";

    private static final String ACTION_MORE = "more";

    private static final String ACTION_LATEST = "latest";

    private static final String PARAM_OAUTH_TOKEN = "oauth_token";

    private static final String PARAM_STATUS = "status";

    private static final String ATTR_REQUEST_TOKEN = "request_token";

    private static final String ATTR_REQUEST_SECRET = "request_secret";

    private static final String ATTR_ACCESS_TOKEN = "access_token";

    private static final String ATTR_ACCESS_SECRET = "access_secret";

    private static final String ATTR_LAST_UPDATED_STATUS = "last_udpated_status";

    private static final String ATTR_LAST_UPDATED_STATUS_ID = "last_udpated_status_id";

    private static final String ATTR_FRIENDS_TIMELINES = "friends_timelines";

    private static final String ATTR_FRIENDS_TIMELINES_PAGE = "friends_timelines_page";

    private static final String PAGE_UPDATE_STATUS = "update_twitter_status.jsp";

    private static final String COOKIE_TWITTER_ID = "twitter_id";

    private static final String SEPARATOR = "&";

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        consumerKey = config.getInitParameter(INIT_PARAM_CONSUMER_KEY);
        if(consumerKey == null || consumerKey.trim().equals("")) {
            logger.severe("Consumer key is not defined in web.xml");
        } else {
            logger.fine("Consumer key retrieved from web.xml");
        }
        consumerSecret = config.getInitParameter(INIT_PARAM_CONSUMER_SECRET);
        if(consumerSecret == null || consumerSecret.trim().equals("")) {
            logger.severe("Consumer secret is not defined in web.xml");
        } else {
            logger.fine("Consumer secret retrieved from web.xml");
        }
    }

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        if(session == null) {
            session = req.getSession(true);
            logger.fine("Session is null, new session created");
        }

        // Call back from Twitter
        String oauthToken = req.getParameter(PARAM_OAUTH_TOKEN);
        if(oauthToken != null) {
            logger.fine("Call back from Twitter, " + PARAM_OAUTH_TOKEN + " received");
            String req_token = (String) session.getAttribute(ATTR_REQUEST_TOKEN);
            String req_secret = (String) session.getAttribute(ATTR_REQUEST_SECRET);
            logger.fine("ATTR_REQUEST_TOKEN retrieved from session");
            if(req_token == null) {
                logger.severe("ATTR_REQUEST_TOKEN is null");
                throw new ServletException("ATTR_REQUEST_TOKEN is null");
            }

            Twitter twitter = new TwitterFactory().getInstance();
            twitter.setOAuthConsumer(consumerKey, consumerSecret);
            try {
                AccessToken accessToken = twitter.getOAuthAccessToken(req_token, req_secret);
                twitter.setOAuthAccessToken(accessToken);

                session.removeAttribute(ATTR_REQUEST_TOKEN);
                session.removeAttribute(ATTR_REQUEST_SECRET);
                logger.fine("ATTR_REQUEST_TOKEN removed from session");

                session.setAttribute(ATTR_ACCESS_TOKEN, accessToken.getToken());
                session.setAttribute(ATTR_ACCESS_SECRET, accessToken.getTokenSecret());
                logger.fine("ATTR_ACCESS_TOKEN saved in session");
                logger.fine("ATTR_ACCESS_SECRET saved in session");

                long id = twitter.verifyCredentials().getId();
                logger.fine("Access token retrieved for user " + id + " from Twitter");

                storeAccessToken("" + id, accessToken);

                // Set cookie, valid for 2 years
                Cookie cookie = new Cookie(COOKIE_TWITTER_ID, "" + id);
                cookie.setMaxAge(63072000); // Valid for 2 years
                resp.addCookie(cookie);
                logger.fine("Cookie set for user " + id);

                // Get last status and friends' timelines
                getMyLastStatusAndStoreInSession(session);
                getFriendsTimelinesAndStoreInSession(session);

                // Go to the update status page
                logger.fine("Going to the update satus page...");
                req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
            } catch (TwitterException e) {
                logger.severe("Failed to retrieve access token - " + e.getMessage());
                throw new ServletException(e);
            }
        }

        String action = req.getParameter(PARAM_ACTION);
        if(ACTION_SIGN_IN.equals(action)) {
            logger.fine("Action: sign in");
            Twitter twitter = new TwitterFactory().getInstance();
            twitter.setOAuthConsumer(consumerKey, consumerSecret);

            // Try to load Twitter ID from cookies
            String id = null;
            Cookie[] cookies = req.getCookies();
            if(cookies != null) {
                Cookie cookie;
                for (int i = 0; i < cookies.length; i++) {
                    cookie = cookies[i];
                    if(COOKIE_TWITTER_ID.equals(cookie.getName())) {
                        id = cookie.getValue();
                        logger.fine("Twitter ID loaded from cookie - " + id);
                    }
                }
            }

            // Try to load access token if user's Twitter ID is retrieved
            AccessToken accessToken = null;
            if(id != null) {
                accessToken = loadAccessToken(id);
                if(accessToken != null) {
                    logger.fine("Access token loaded from datastore");

                    session.setAttribute(ATTR_ACCESS_TOKEN, accessToken.getToken());
                    session.setAttribute(ATTR_ACCESS_SECRET, accessToken.getTokenSecret());

                    logger.fine("ATTR_ACCESS_TOKEN saved in session");
                    logger.fine("ATTR_ACCESS_SECRET saved in session");

                    twitter.setOAuthAccessToken(accessToken);
                    // Get last status and friends' timelines
                    try {
                        getMyLastStatusAndStoreInSession(session);
                        getFriendsTimelinesAndStoreInSession(session, true);
                    } catch (TwitterException e) {
                        throw new ServletException(e);
                    }

                    // Go to the update status page
                    logger.fine("Going to the update satus page...");
                    req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
                }
            }

            // Can not load access token, go to Twitter for authentication
            if(accessToken == null) {
                try {
                    RequestToken requestToken = twitter.getOAuthRequestToken();
                    String authorisationUrl = requestToken.getAuthorizationURL();

                    session.setAttribute(ATTR_REQUEST_TOKEN, requestToken.getToken());
                    session.setAttribute(ATTR_REQUEST_SECRET, requestToken.getTokenSecret());

                    logger.fine("ATTR_REQUEST_TOKEN saved in session");
                    logger.fine("ATTR_REQUEST_SECRET saved in session");
                    logger.fine("Redirecting user to " + authorisationUrl);
                    resp.sendRedirect(authorisationUrl);
                } catch (TwitterException e) {
                    logger.fine("Sign in with Twitter failed - " + e.getMessage());
                    throw new ServletException(e);
                }
            }

        } else if(ACTION_UPDATE.equals(action)) {
            logger.fine("Action: update");
            String status = req.getParameter(PARAM_STATUS);
            if(status != null && status.trim() != "") {
                Twitter twitter = generateTwitterObjectFromSession(session);

                try {
                    long id = twitter.verifyCredentials().getId();
                    Status twitterStatus = twitter.updateStatus(status);
                    session.setAttribute(ATTR_LAST_UPDATED_STATUS, twitterStatus.getText() + " " + twitterStatus.getCreatedAt());
                    session.setAttribute(ATTR_LAST_UPDATED_STATUS_ID, "" + twitterStatus.getId());

                    // Update friends' timelines
                    getFriendsTimelinesAndStoreInSession(session, true);

                    logger.fine("Status updated for Twitter user " + id + " to [" + twitterStatus.getText() + "], status stored in session as well");
                } catch (TwitterException e) {
                    logger.severe("Failed to update Twitter status - " + e.getMessage());
                    throw new ServletException(e);
                }
            }

            // Stay in the update status page
            logger.fine("Staying in the status update page...");
            req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
        } else if(ACTION_DELETE.equals(action)) {
            logger.fine("Action: delete");
            String strId = (String) session.getAttribute(ATTR_LAST_UPDATED_STATUS_ID);
            if(strId != null && strId != "") {
                Twitter twitter = generateTwitterObjectFromSession(session);
                try {
                    long id = twitter.verifyCredentials().getId();
                    twitter.destroyStatus(Long.parseLong(strId));
                    session.removeAttribute(ATTR_LAST_UPDATED_STATUS);
                    session.removeAttribute(ATTR_LAST_UPDATED_STATUS_ID);
                    logger.fine("Last update deleted for Twitter user " + id + " deleted, session record removed");

                    // Get last status and friends' timelines
                    getMyLastStatusAndStoreInSession(session);
                    getFriendsTimelinesAndStoreInSession(session, true);
                } catch (TwitterException e) {
                    logger.severe("Failed to delete Twitter status - " + e.getMessage());
                    throw new ServletException(e);
                }
            }

            // Stay in the update status page
            logger.fine("Staying in the status update page...");
            req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
        } else if(ACTION_MORE.equals(action)) {
            // Update friends' timelines
            getFriendsTimelinesAndStoreInSession(session);

            // Stay in the update status page
            logger.fine("Staying in the status update page...");
            req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
        } else if(ACTION_LATEST.equals(action)) {
            // Update friends' latest timelines
            getFriendsTimelinesAndStoreInSession(session, true);

            // Stay in the update status page
            logger.info("Staying in the status update page...");
            req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
        }
    }

    private Twitter generateTwitterObjectFromSession(HttpSession session) throws ServletException {
        String at = (String) session.getAttribute(ATTR_ACCESS_TOKEN);
        logger.fine("ATTR_ACCESS_TOKEN retrieved from session");
        if(at == null) {
            logger.severe("ATTR_ACCESS_TOKEN is null");
            throw new ServletException("ATTR_ACCESS_TOKEN is null");
        }
        String[] attts = at.split(SEPARATOR);
        AccessToken accessToken = new AccessToken(attts[0], attts[1]);

        Twitter twitter = new TwitterFactory().getInstance();
        twitter.setOAuthConsumer(consumerKey, consumerSecret);
        twitter.setOAuthAccessToken(accessToken);

        return twitter;
    }

    private void getMyLastStatusAndStoreInSession(HttpSession session) throws TwitterException, ServletException {
        Twitter twitter = generateTwitterObjectFromSession(session);

        // Get last updated status and save it in session
        List<Status> updatesList = twitter.getUserTimeline();
        if(!updatesList.isEmpty()) {
            Status status = updatesList.get(0);
            session.setAttribute(ATTR_LAST_UPDATED_STATUS, status.getText() + " [" + status.getCreatedAt() + "]");
            session.setAttribute(ATTR_LAST_UPDATED_STATUS_ID, "" + status.getId());
            logger.fine("Last update stored in session for Twitter user " + twitter.verifyCredentials().getId());
        } else {
            logger.fine("Twitter user " + twitter.verifyCredentials().getId() + " has no update available");
        }
    }

    private void getFriendsTimelinesAndStoreInSession(HttpSession session, boolean isLatestTimelinesRequired) throws ServletException {
        if(isLatestTimelinesRequired) { // Start from page 1
            session.removeAttribute(ATTR_FRIENDS_TIMELINES_PAGE);
            getFriendsTimelinesAndStoreInSession(session);
        }
    }

    private void getFriendsTimelinesAndStoreInSession(HttpSession session) throws ServletException {
        Integer p = (Integer) session.getAttribute(ATTR_FRIENDS_TIMELINES_PAGE);
        int page = 1;
        if(p != null) {
            page = p + 1;
        }

        Twitter twitter = generateTwitterObjectFromSession(session);

        try {
            List<Status> l = twitter.getFriendsTimeline(new Paging(page, 20));
            TwitterStatus[] tss = new TwitterStatus[l.size()];
            for (int i = 0; i < tss.length; i++) {
                Status s = l.get(i);
                TwitterStatus ts = new TwitterStatus();
                ts.setCreatedDate(s.getCreatedAt());
                ts.setCreatedUser(s.getUser().getScreenName());
                ts.setId(s.getId());
                ts.setMessage(searchAndReplaceLink(searchAndReplaceLink(s.getText(), "http"), "https"));
                tss[i] = ts;
            }
            logger.info(tss.length + " friends' timeline retrieved, page number is " + page);

            session.setAttribute(ATTR_FRIENDS_TIMELINES, tss);
            session.setAttribute(ATTR_FRIENDS_TIMELINES_PAGE, page);
        } catch (TwitterException te) {
            try {
                logger.severe("Failed to get friends' timeline for " + twitter.getId() + " - " + te.getMessage());
            } catch (Exception e) {}
        }
    }

    private static String searchAndReplaceLink(String message, String protocol) {
        // Message returned by Twitter does not have proper HTTP(S) links
        // So construct them here
        if(message == null || protocol == null) {
            return message;
        }

        protocol = protocol.trim();
        if(protocol.indexOf("://") == -1) {
            protocol = " " + protocol + "://";
        }

        int idx1, idx2;
        StringBuilder sb = new StringBuilder();
        while ((idx1 = message.indexOf(protocol)) != -1) {
            sb.append(message.substring(0, idx1 + 1));
            message = message.substring(idx1 + 1);
            if((idx2 = message.indexOf(' ')) != -1) {
                String str = message.substring(1, idx2);
                sb.append("<a href=\"").append(str).append("\" target=\"_blank\">").append(str).append("</a>");
                message = message.substring(idx2);
            } else {
                sb.append("<a href=\"").append(message).append("\" target=\"_blank\">").append(message).append("</a>");
                message = "";
            }
        }
        sb.append(message);

        return sb.toString();
    }

    private void storeAccessToken(String id, AccessToken accessToken) {
        if(accessToken == null) {
            throw new IllegalArgumentException("Access token to be saved is null");
        }

        // Notice id starts with a digit number, not allowed
        IdAndAccessToken t = new IdAndAccessToken('$' + id, accessToken.getToken(), accessToken.getTokenSecret());
        EntityManager em = JpaUtil.getEmf().createEntityManager();
        try {
            em.getTransaction().begin();
            em.persist(t);
            em.getTransaction().commit();
            logger.fine("Access token saved to datastore for Twitter user " + id);
        } finally {
            em.close();
        }
    }

    private AccessToken loadAccessToken(String id) {
        if(id == null || id.trim().equals("")) {
            throw new IllegalArgumentException("ID to be queried is null");
        }
        EntityManager em = JpaUtil.getEmf().createEntityManager();
        try {
            em.getTransaction().begin();
            // Notice id starts with a digit number, not allowed
            IdAndAccessToken t = em.find(IdAndAccessToken.class, '$' + id);
            em.getTransaction().commit();
            logger.fine("Access token loaded from datastore for Twitter user " + id);
            return new AccessToken(t.getToken(), t.getTokenSecret());
        } finally {
            em.close();
        }
    }
}
